# -*- coding:utf-8 -*-
"""
@Author : 审心
@Time : 2023/10/24 14:08
https://sycm.taobao.com/mc/mq/market_rank?activeKey=item
市场排行 -> 商品标签页 相关的自动化
"""
import os.path
import random
import re
import time
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import List, Optional, Callable

from DrissionPage import WebPage
from DrissionPage.action_chains import ActionChains
from DrissionPage.commons.keys import Keys
from openpyxl.utils import get_column_letter
from xlwings import App, Sheet

import config
from autoTask.taobao.enum import WebPlugin
from autoTask.taobao.sycm.market_rank import util
from autoTask.taobao.sycm.utils import WaitUtil
from autoTask.taobao.sycm.utils.page_action import PageAction
from common import redis
from common.logger.log import log_
from common.redis import redis_client
from common.utils import ExcelUtil, FileUtil, Base64Util
from common.utils.DateUtil import seconds_until_end_of_day
from common.utils.ExcelUtil import XwRowMap, xw_get_has_content_row
from common.utils.web_page_util import MockTap, Tap


@dataclass
class CacheAction:
    @staticmethod
    def get_key(cycle, category):
        """
        获取存储key
        :param cycle: 周期
        :param category: 品类
        :return:
        """
        return config.data.get("redis.keyPrefix") + f"淘系:新品筛选功能:{cycle}:{category}"

    @staticmethod
    def put(cycle, category, path):
        """
        追加到缓存
        :param cycle: 周期
        :param category: 类目
        :param path: 表格文件路径
        :return:
        """
        from common.redis import redis_client
        name = os.path.basename(path)
        content = Base64Util.file_to_base64(path)
        redis_client.put(CacheAction.get_key(cycle, category), {
            "name": name,
            "content": content
        }, seconds_until_end_of_day(1))

    @staticmethod
    def download(cycle, category) -> str:
        """
        从缓存中获取表格到本地
        :param cycle: 周期
        :param category: 类目
        :return: 文件路径
        """
        value: dict = redis_client.get(CacheAction.get_key(cycle, category), dict)
        if value:
            return Base64Util.base64_to_file(value.get("content"), ExcelAction.get_temp_file_path(), value.get("name"))
        return None


@dataclass
class FilterParam:
    # 支付人数 小于此值的去除
    mini_payer: int
    # g1 组 交易金额 小于此值的去除
    g1_mini_amount: float
    # g1 组 uv 价值 小于此值的去除
    g1_mini_uv: float

    def mini_payer_compare(self, titles) -> Callable:
        return lambda row: int(row[titles.index("支付人数")]) < self.mini_payer

    def g1_compare(self, titles) -> Callable:
        def compare(row):
            return float(row[titles.index("交易金额")]) < self.g1_mini_amount \
                   and float(row[titles.index("UV价值")]) < self.g1_mini_uv

        return compare


@dataclass
class ExcelAction:
    # 表格路径
    paths: List[str]
    # excel 示例
    excel_app: App
    # 过滤参数
    filter_param: FilterParam

    @staticmethod
    def result_file_name():
        return "合并后表格.xlsx"

    @staticmethod
    def result_file_path():
        return ExcelAction.get_temp_file_path() + fr"\{ExcelAction.result_file_name()}"

    @staticmethod
    def clear_excel():
        FileUtil.clear_folder(ExcelAction.get_temp_file_path())

    def __call__(self):
        if os.path.exists(ExcelAction.result_file_path()):
            os.remove(ExcelAction.result_file_path())
        ExcelUtil.xlsx_merge(self.paths, "合并后表格", ExcelAction.get_temp_file_path(),
                             ExcelAction.result_file_name(), True)
        # 待机一下 防止合并表格结束处理的造成的异常
        time.sleep(15)
        sht: Sheet = self.excel_app.books.open(ExcelAction.result_file_path()).sheets[0]
        # 删除无用列
        if "创建时间" in sht.range("1:1").value:
            sht.api.Columns(sht.range("1:1").value.index("创建时间") + 1).Delete()
            sht.book.save()
        # 过滤
        self.filter(sht, sht.range("1:1").value)
        # 调整格式保存
        sht.autofit()
        sht.book.save()
        sht.book.close()

    def filter(self, sht: Sheet, titles):
        """
        过滤表格数据
        :return:
        """
        last_row_num = ExcelUtil.xw_get_has_content_row(sht, get_column_letter(titles.index("商品信息") + 1))
        last_col_num = ExcelUtil.xw_get_last_used_column(sht, 1)
        row_map: List[XwRowMap] = XwRowMap.builder_by_rows(
            sht.range(f"A2:{last_col_num + str(last_row_num)}"))
        # 支付人数过滤
        filter1: List[XwRowMap] = XwRowMap. \
            filter_row_map_by_row(row_map, self.filter_param.mini_payer_compare(titles))
        # 组 1 过滤
        filter2: List[XwRowMap] = XwRowMap. \
            filter_row_map_by_row(row_map, self.filter_param.g1_compare(titles))
        # 除去交集行号
        del_rows = list(set(filter1).union(set(filter2)))
        # 行号降序从底部开始删除 避免删除时缩进带来的
        for row in sorted([row.num for row in del_rows], reverse=True):
            ExcelUtil.xw_delete_row_by_row_num(sht, row)
            ExcelUtil.xw_del_sht_img_by_name(sht, f"Picture {row - 1}")

    def append_category(self):
        """
        追加类目列
        :return:
        """
        from xlwings import Sheet
        from common.utils import ExcelUtil
        for path in self.paths:
            sheet: Sheet = self.excel_app.books.open(path).sheets[0]
            try:
                # 新列字母
                col = chr(ord(ExcelUtil.xw_get_last_used_column(sheet, 0)) + 1)
                # 需要填充的行数
                fill_row = xw_get_has_content_row(sheet, 'C')
                # 类目
                category = ExcelAction.get_export_file_category(path)
                # 初始化插入列表
                fill_data = ["类目"]
                for _ in range(0, fill_row - 1):
                    fill_data.append(category)
                sheet.range(f"{col}1:{col}{fill_row}").options(transpose=True).value = fill_data
            finally:
                sheet.book.save()
                sheet.book.close()

    @staticmethod
    def get_temp_file_path() -> str:
        import os
        return os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_file")

    @staticmethod
    def get_now_export_file_path(timeout=30, category: str = None) -> List[str]:
        """
        获取刚(今天)导出的表格文件路径
        :param timeout:超时时间
        :param category: 类目
        :return:
        """
        from common.utils import FileUtil
        # 导出文件的文件夹路径
        dir_path = ExcelAction.get_temp_file_path()
        # 昨日日期字符串
        yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        # 文件匹配表达式
        match = f"*{category}*{yesterday}*" if category else f"*{yesterday}*"
        return FileUtil.find_file(dir_path, match, timeout)

    @staticmethod
    def get_export_file_category(path) -> Optional[str]:
        """
        阿明工具. 获取导出文件类目
        :param path: 文件路径
        :return:
        """
        m = re.search(r'_([^_]+)】', path)
        if m:
            return m.group(1).strip()
        return None


@dataclass
class MarketRankItemWebAction(PageAction):
    """
    商品排行界面相关操作
    https://sycm.taobao.com/mc/mq/market_rank?activeKey=item
    """

    def set_category(self, category: str):
        """
        设置页面类目
        :param category: 品类字符串
        :return:
        """
        if not self.p(f"@@class=common-picker-header@@title:{category}"):
            # 点击下拉框
            MockTap(self.p(".common-picker-header"), "打开设置商品类型下拉窗失败", after_delay=3)()
            # 输入关键词搜索
            self.p("@@class=ant-input@@placeholder:请输入关键字").input(category)
            time.sleep(3)
            # 点击对应关键词
            MockTap(self.p(f"@@class^oui-typeahead-dropdown-item@@text():{category}"),
                    "点击搜索结果中的关键词失败")()
        return self

    def set_date_cycle(self, date_cycle: str) -> str:
        """
        设置数据周期
        :param date_cycle: 7天/30天
        :return: 真正使用数据周期 (存在账号不支持查看30天数据)
        """
        e = None
        if date_cycle == '30天':
            e = self.p("text=30天", timeout=5)
        elif date_cycle == '7天':
            e = self.p("text=7天", timeout=5)
        if not e and date_cycle != '7天':
            e = self.p("text=7天", timeout=5)
        # 已经被选中了不再点击
        if e.parent().attr("class") != "ant-btn oui-canary-btn ant-btn-primary ant-btn-sm":
            MockTap(e, "设置数据周期失败")()
            # 等待加载完毕
            WaitUtil.wait_tb_complete(self.p)
        return e.text

    def pile_up_view_data(self, retry_count=5):
        """
        堆积页面数据 即, 轮询页面 高交易/高流量/高意向 标签页
        :return:
        """
        tabs = ["高流量", "高意向", "高交易"]
        # 打乱一下点击顺序
        random.shuffle(tabs)
        for (item) in tabs:
            MockTap(self.p(f"@@class^oui-tab-switch-item@@text():{item}"),
                    f"点击跳转到{item}界面失败")()
            WaitUtil.wait_tb_complete(self.p)
            # 避免没有加载出数据的情况 重试方法5次
            if self.p("text:人山人海", timeout=3) and retry_count > 0:
                self.pile_up_view_data(retry_count - 1)
                break
        return self

    def plugin_open_merge_conversion(self, plugin: WebPlugin):
        """
        打开插件合并转化
        :return:
        """
        m = None
        if WebPlugin.XSW == plugin:
            m = "//div[@id='xws-commodity-deep-analysis']//span[1]/span[.='合并转化']"
        elif WebPlugin.AM == plugin:
            m = "//button[@id='marketRankListMergeTransform']"
        else:
            raise RuntimeError("不支持的游览器插件")
        e = None
        # 防止 插件按钮突然加载不出
        for _ in range(0, 5):
            e = self.p(f"x:{m}")
            if e:
                break
            self.p.refresh()
            self.p.wait.load_complete()
        Tap(e, "打开插件合并转化失败",is_random=False)()
        return self

    def plugin_export_merge_conversion(self, plugin: WebPlugin, export_path: str):
        """
        插件合并转化 导出数据
        :param plugin: 插件枚举
        :param export_path : 导出路径
        :return:
        """
        m = None
        if WebPlugin.XSW == plugin:
            m = "//li[.='导出xlsx表格（带图片）']"
        elif WebPlugin.AM == plugin:
            m = "//span[.='导出XLSX表格（所有列）（含图片）']"
        else:
            raise RuntimeError("不支持的游览器插件")
        self.p.download_set.save_path(export_path)
        Tap(self.p(f"x:{m}"), "导出数据失败", before_delay=[2, 3], is_random=False)()
        self.p.wait.download_begin(30)
        ActionChains(self.p).key_down(Keys.ESCAPE)
        ActionChains(self.p).key_up(Keys.ESCAPE)
        return self


@dataclass
class Main1:
    """
    任务来自于 2023/10/23
    优姐的需求 , 任务名称 : 淘系新品筛选功能
    大致是 配合阿明插件 获取市场排行 商品标签中的商品数据
    ps :
    1. 高交易/高流量/高意向 ->  阿明合并转化
    2. 复制转化数据(带图片) 至 excel 表
    3. 追加一列内容为当前品类
    4. 图片要求使用 wps 嵌入表格
    5. 可以获取多个类目的数据, 多个类目数据逐步追加至结果表格表格
    """
    category_list: List
    date_cycle: str
    p: WebPage
    plugin: WebPlugin
    filter_param: FilterParam
    temp_file_path: str = util.get_temp_file_path()

    def __post_init__(self):
        # 关闭所有excel 进程
        ExcelUtil.xw_close_all_excel()
        self.web_action = MarketRankItemWebAction(self.p)
        # 清除所有表格
        ExcelAction.clear_excel()

    def __call__(self) -> str:
        import xlwings as xw
        use_cycle = self.web_action.goto_index().goto_market().goto_market_rank_item().set_date_cycle(self.date_cycle)
        # 导出各个品类文件
        for category in self.category_list:
            # 直接从缓存中导出
            if CacheAction.download(use_cycle, category):
                continue
            self.web_action.set_category(category).pile_up_view_data()
            # 导出
            self.web_action.plugin_open_merge_conversion(self.plugin) \
                .plugin_export_merge_conversion(self.plugin, self.temp_file_path)
            # 将类目原本的 / 替换为 _ 匹配导出表格文件名
            paths = ExcelAction.get_now_export_file_path(60, category.replace('/', '_'))
            if len(paths) == 0:
                log_.warning(f"导出品类 : [{category}] 数据表格失败")
            else:
                # 添加到缓存
                CacheAction.put(use_cycle, category, paths[0])
        # 开始处理导出的表格
        app = xw.App(visible=False, add_book=False)
        try:
            ExcelAction(ExcelAction.get_now_export_file_path(0), app, self.filter_param)()
        finally:
            app.quit()
        if not os.path.exists(ExcelAction.result_file_path()):
            raise RuntimeError("没有发现合并后的表格")
        return ExcelAction.result_file_path()


if __name__ == '__main__':
    pass
